﻿Imports System.Text.RegularExpressions
Imports Autodesk.AutoCAD.EditorInput
'
' Flexible picture format for auto-numbering
' ==========================================
'
' The format picture defines absolutely the expected format for a field, 
' and sets the "number of components" field automatically.
'
' The format picture is made up of delimiters, characters and wild-cards.
' - Characters are A-Z,a-z,0-9,_
' - Wild-card is %
'
' Delimiters are anything else, including spaces.
'
' Fixed-length components are defined by one or more characters, and are 
' delimited either by an explicit delimiter, or by a different character, 
' or by the end of the picture.
'
' Variable-length fields are defined by a wild-card, and must be delimited 
' by a delimiter or by the end of the picture.
'
' XYZZ%-% defines a 5-component field, where the first three are fixed-width 
' of 1, 1 and 2 characters, and the final two are variable width separated 
' by "-".
'
Public Class CLDS_Flex_Format
    Private a_picture As String                                 ' Flexible format picture (c.f. PKG_ICE_UDFORMATS.UFM_PICTURE)
    Private ufm_code As String = ""                             ' CMPIC rule name
    Private a_regex As String                                   ' Regular expression syntax equivalent for format picture
    Private a_token(100) As String                              ' Array of token names
    Private a_delim(100) As String                              ' Array of token delimiters
    Private a_len(100) As Integer                               ' Array of token lengths
    Private a_key(100) As Boolean                               ' Array of tokey key switches (TRUE=part of uniqueness, FALSE=not)
    Private n_fields As Integer                                 ' Number of fields in format picture
    Private k_fields As Integer                                 ' Number of key fields in format picture
    Private a_value(100) As String                              ' Array of value fields
    Private d_field As System.Collections.Generic.Dictionary(Of String, String) ' Bound format values, keyed by picture character
    Private d_prompt As System.Collections.Generic.Dictionary(Of String, String) ' Prompt values, keyed by picture character
    Private d_valid As System.Collections.Generic.Dictionary(Of String, String) ' Valid values, comma separated, keyed by picture character
    Private d_brace As System.Collections.Generic.Dictionary(Of String, String) ' Variable-length fields specified by curly braces, keyed by picture character
    Private n_minno As Integer = 1                              ' Start number for auto-generation
    Public Sub New(ByVal p As String, Optional ByVal ufm As String = "", Optional ByVal start As Integer = 1)
        d_field = New System.Collections.Generic.Dictionary(Of String, String)
        d_prompt = New System.Collections.Generic.Dictionary(Of String, String)
        d_valid = New System.Collections.Generic.Dictionary(Of String, String)
        d_brace = New System.Collections.Generic.Dictionary(Of String, String)
        ufm_code = ufm
        Dim x As String = p
        Dim o As Boolean = False
        If InStr(x, "{") > 0 Then
            x = ConvertPicture(x)
            o = True
        End If
        Initialise(x)
        If o Then
            ConvertRule()
        End If
        n_minno = start
    End Sub
    Public Sub Initialise(ByVal p As String)
        a_picture = p
        Split_Format(p, "Y")
        a_regex = Picture_Match(p)
        Prompt("A") = "Area"
        Prompt("I") = "Node ID"
        Prompt("N") = "Auto-generated number"
        Prompt("P") = "Point-segment type"
        Prompt("R") = "Raceway ID"
        Prompt("S") = "Segregation"
        Prompt("T") = "Segment type"
        For n As Integer = 1 To n_fields
            If Not d_prompt.ContainsKey(a_token(n)) Then
                Prompt(a_token(n)) = "Field " & Trim(Str$(n))
            End If
        Next
    End Sub
    Public ReadOnly Property NoFields() As Integer
        Get
            Return n_fields
        End Get
    End Property
    Public ReadOnly Property UfmCode() As String
        Get
            Return ufm_code
        End Get
    End Property
    Public ReadOnly Property Picture() As String
        Get
            Return a_picture
        End Get
    End Property
    Public ReadOnly Property Delim(ByVal i As Integer) As String
        Get
            If i <= n_fields Then
                Return a_delim(i)
            Else
                Return ""
            End If
        End Get
    End Property
    Public Property IsKey(ByVal i As Integer) As Boolean
        Get
            If i <= n_fields Then
                Return a_key(i)
            Else
                Return True
            End If
        End Get
        Set(ByVal value As Boolean)
            If i <= n_fields Then
                a_key(i) = value
                k_fields = 0
                For n As Integer = 1 To n_fields
                    If a_key(n) Then
                        k_fields += 1
                    End If
                Next
            End If
        End Set
    End Property
    Public Property Token(ByVal i As Integer) As String
        Get
            If i <= n_fields Then
                Return a_token(i)
            Else
                Return ""
            End If
        End Get
        Set(ByVal value As String)
            If i <= n_fields Then
                If a_token(i) = "" Then
                    a_token(i) = value
                End If
            End If
        End Set
    End Property
    Public Property Field(ByVal i As Integer) As String
        Get
            If i <= n_fields Then
                Return a_value(i)
            Else
                Return ""
            End If
        End Get
        Set(ByVal value As String)
            If i <= n_fields Then
                a_value(i) = value
            End If
        End Set
    End Property
    Public Property Prompt(ByVal n As String) As String
        Get
            If d_prompt.ContainsKey(n) Then
                Return d_prompt(n)
            Else
                Return ""
            End If
        End Get
        Set(ByVal value As String)
            If d_prompt.ContainsKey(n) Then
                d_prompt(n) = value
            Else
                d_prompt.Add(n, value)
            End If
        End Set
    End Property
    Public Property Valid(ByVal n As String) As String
        Get
            If d_valid.ContainsKey(n) Then
                Return d_valid(n)
            Else
                Return ""
            End If
        End Get
        Set(ByVal value As String)
            If d_valid.ContainsKey(n) Then
                d_valid(n) = value
            Else
                d_valid.Add(n, value)
            End If
        End Set
    End Property
    Public Property Bind(ByVal n As String) As String
        Get
            If d_field.ContainsKey(n) Then
                Return d_field(n)
            Else
                Return ""
            End If
        End Get
        Set(ByVal value As String)
            If d_field.ContainsKey(n) Then
                d_field(n) = value
            Else
                d_field.Add(n, value)
            End If
        End Set
    End Property
    Public Property StartNo() As Integer
        Get
            Return n_minno
        End Get
        Set(ByVal value As Integer)
            n_minno = value
        End Set
    End Property
    Public Function MaxNo() As Integer
        Dim m As Integer = 1
        For n As Integer = 1 To n_fields
            If UCase(a_token(n)) = "N" Then
                If a_len(n) > 0 Then
                    m = (10 ^ a_len(n)) - 1
                Else
                    m = 9999
                End If
                Return m
            End If
        Next
        Return m
    End Function
    Public Function Is_Delimiter(ByVal c As Char) As Boolean
        If UCase(c) >= "A" And UCase(c) <= "Z" Then
            Return False
        End If
        If c >= "0" And c <= "9" Then
            Return False
        End If
        If c = "%" Or c = "_" Then
            Return False
        End If
        Return True
    End Function
    Public Function IndexFor(ByVal t As Char) As Integer
        For n As Integer = 1 To n_fields
            If UCase(a_token(n)) = t Then
                Return n
            End If
        Next
        Return 0
    End Function
    Public Sub Split_Format(ByVal s As String, Optional ByVal x As String = "N")
        '
        ' Parse a format picture into token names, lengths and delimiters
        '
        Dim i As Integer, l As Integer
        Dim c As Char, f As Char

        For i = 1 To 100
            a_token(i) = ""
            a_delim(i) = ""
            a_len(i) = 0
            a_key(i) = True
        Next
        n_fields = 0
        k_fields = 0

        If s = "" Then
            Return
        End If

        i = 1
        l = Len(s)

        While i <= l
            c = Mid$(s, i, 1)
            If Is_Delimiter(c) Then
                If x = "Y" Then
                    MsgBox("Misplaced delimiter " & c, MsgBoxStyle.Critical, "CMPIC AutoCAD")
                End If
                n_fields = -1
                Return
            End If
            n_fields += 1
            a_len(n_fields) = 1
            f = c
            If c = "%" Then
                a_len(n_fields) = 0
                i += 1
                If i > l Then
                    a_delim(n_fields) = ""
                Else
                    c = Mid$(s, i, 1)
                    If Not Is_Delimiter(c) Then
                        If x = "Y" Then
                            MsgBox("Missing delimiter at position " & Str$(i), MsgBoxStyle.Critical, "CMPIC AutoCAD")
                        End If
                        n_fields = -2
                        Return
                    End If
                    a_delim(n_fields) = c
                    i += 1
                End If
                'Prompt(n_fields) = "Field " & Trim(Str$(n_fields))
            Else
                a_token(n_fields) = f
                If UCase(f) = f Or UCase(f) = "N" Then
                    IsKey(n_fields) = True
                Else
                    IsKey(n_fields) = False
                End If
                Dim z As Boolean = True
                Do While z
                    i += 1
                    If i > l Then
                        z = False
                    Else
                        c = Mid$(s, i, 1)
                        If c <> f Then
                            z = False
                        Else
                            a_len(n_fields) += 1
                        End If
                    End If
                Loop
                a_delim(n_fields) = ""
                If i < l Then
                    If Is_Delimiter(c) Then
                        a_delim(n_fields) = c
                        i += 1
                    End If
                End If
            End If
        End While
        Debug.Print(" ")
        Debug.Print("Format " & s)
        For i = 1 To n_fields
            Debug.Print("Field " & Str$(i) & " length " & Str$(a_len(i)) & " delimiter " & a_delim(i))
        Next
        Return
    End Sub
    Public Function Picture_Match(ByVal s As String) As String
        Dim i As Integer, l As Integer
        Dim m As String
        Dim c As Char
        m = ""
        l = Len(s)
        For i = 1 To l
            c = Mid$(s, i, 1)
            If Is_Delimiter(c) Then
                m = m & c
            ElseIf c = "%" Then
                m = m & c
            Else
                m = m & "_"
            End If
        Next
        Return m
    End Function
    Public Function Split_Value(ByVal v As String) As String
        '
        ' Use parsed format to split a value into its constituent fields
        '
        Dim i As Integer, c As Integer, l As Integer, d As Integer
        If v = "" Or n_fields = 0 Then
            a_value(1) = ""
            Return "OK"
        End If
        'Dim m As Match = Regex.Match(v, a_regex, RegexOptions.IgnoreCase)
        'If Not (m.Success) Then
        'Return "ERROR: value " & v & " does not match format picture " & a_picture
        'End If
        c = 1
        l = Len(v)
        For i = 1 To n_fields
            a_value(i) = ""
            If a_len(i) = 0 Then
                If a_delim(i) = "" Then
                    a_value(i) = Mid$(v, c)
                    c = l + 1
                Else
                    d = InStr(c, v, a_delim(i), CompareMethod.Text)
                    If d = 0 Then
                        Return "ERROR: expected delimiter " & a_delim(i) & " missing"
                    Else
                        a_value(i) = Mid$(v, c, d - c)
                        c = d + 1
                    End If
                End If
            Else
                If c + a_len(i) - 1 > l Then
                    Return "ERROR: truncated fixed-length field"
                Else
                    a_value(i) = Mid$(v, c, a_len(i))
                    c = c + a_len(i)
                    If Not a_delim(i) = "" Then
                        c = c + 1
                    End If
                End If
            End If
        Next
        Return "OK"
    End Function
    Public Sub Binds(ByVal n As Integer)
        '
        ' Bind values into constituent fields from corresponding token.
        ' Field "N" is bound using parameter n, left zero-padded if necessary
        '
        Dim i As Integer
        For i = 1 To n_fields
            If a_token(i) <> "" Then
                If UCase(a_token(i)) = "N" Then
                    Dim v As String = Trim$(Str$(n))
                    If a_len(i) > 0 And Len(v) < a_len(i) Then
                        v = Mid$("0000000000", 1, a_len(i) - Len(v)) & v
                    End If
                    a_value(i) = v
                ElseIf d_field.ContainsKey(a_token(i)) Then
                    a_value(i) = d_field(a_token(i))
                End If
            End If
        Next
    End Sub
    Public Function Combine_All() As String
        '
        ' Combine all fields and delimiters into final value
        '
        Dim n As Integer
        Dim v As String
        v = ""
        For n = 1 To n_fields
            v = v & a_value(n) & a_delim(n)
        Next
        Return Trim(v)
    End Function
    Public Function Combine_Key() As String
        '
        ' Combine key fields and delimiters into final value, with "XXX" substituted for non-key fields
        '
        Dim n As Integer
        Dim v As String
        Dim f As String
        v = ""
        For n = 1 To n_fields
            If a_key(n) Then
                v = v & a_value(n) & a_delim(n)
            Else
                f = "X"
                If a_len(n) > 1 Then
                    f = Mid$("XXXXXXXXXX", 1, a_len(n))
                End If
                v = v & f & a_delim(n)
            End If
        Next
        Return Trim(v)
    End Function
    Public Function Bind_All(ByVal n As Integer) As String
        Binds(n)
        Return Combine_All()
    End Function
    Public Function Bind_Key(ByVal n As Integer) As String
        Binds(n)
        Return Combine_Key()
    End Function
    Public Function Key_Value(ByVal s As String, Optional ByVal c As Boolean = False) As String
        '
        ' Convert value into key value if not all fields are key fields
        '
        If k_fields = n_fields And Not c Then
            Return s
        End If
        Dim r As String = Split_Value(s)
        If r = "OK" Then
            If k_fields = n_fields Then
                If c Then
                    Return Combine_All()
                Else
                    Return s
                End If
            Else
                Return Combine_Key()
            End If
        Else
            Return s
        End If
    End Function
    Public Function Prompt_All(ByVal p_ed As Editor) As String
        '
        ' Prompt for user-defined field value
        '
        If n_fields = 0 Then
            Return "ERROR: no format rule defined"
        End If
        For n As Integer = 1 To n_fields
            If UCase(a_token(n)) = "A" Or UCase(a_token(n)) = "P" Or UCase(a_token(n)) = "R" Or UCase(a_token(n)) = "S" Or UCase(a_token(n)) = "T" Then
                If Bind(a_token(n)) = "" Then
                    Return "ERROR: " & d_prompt(a_token(n)) & " not specified"
                End If
                If a_len(n) > 0 And Len(Bind(a_token(n))) > a_len(n) Then
                    Return "ERROR: " & d_prompt(a_token(n)) & " length exceeds maximum " & Trim(Str$(a_len(n)))
                End If
            ElseIf UCase(a_token(n)) <> "N" Then
                Dim asking As Boolean = True
                If d_valid.ContainsKey(a_token(n)) Then
                    Dim v As String = d_valid(a_token(n))
                    If v <> "" And InStr(v, ",") = 0 Then
                        Bind(a_token(n)) = v
                        asking = False
                    End If
                End If
                If asking Then
                    Dim m As String = d_prompt(a_token(n))
                    If Not Bind(a_token(n)) = "" Then
                        m = m & " [" & Bind(a_token(n)) & "]"
                    End If
                    m = m & ": "
                    Dim pso As PromptStringOptions = New PromptStringOptions(vbLf & m)
                    pso.AllowSpaces = True
                    While asking
                        Dim psr As PromptResult = p_ed.GetString(pso)
                        If psr.Status = PromptStatus.OK Then
                            If psr.StringResult = "" Then
                                If Bind(a_token(n)) = "" Then
                                    Return "QUIT"
                                End If
                            Else
                                Bind(a_token(n)) = UCase(psr.StringResult)
                            End If
                        Else
                            Return "QUIT"
                        End If
                        If a_len(n) > 0 And Len(Bind(a_token(n))) > a_len(n) Then
                            MsgBox("ERROR: " & d_prompt(a_token(n)) & " length exceeds maximum " & Trim(Str$(a_len(n))), MsgBoxStyle.Critical, "CMPIC AutoCAD")
                        Else
                            asking = False
                        End If
                    End While
                End If
            End If
        Next
        Return "OK"
    End Function
    Public Function ConvertPicture(ByVal p As String) As String
        Dim x As String = p
        x = Replace(x, "{AREA}", "{A}", 1)
        x = Replace(x, "{ID}", "{I}", 1)
        x = Replace(x, "{RWAY}", "{R}", 1)
        x = Replace(x, "{SEG}", "{S}", 1)
        x = Replace(x, "{TYPE}", "{T}", 1)
        x = Replace(x, "{PS}", "{P}", 1)
        x = Replace(x, "{RWAY}", "{R}", 1)
        x = Replace(x, "{NNNNN}", "BBBBB", 1)
        x = Replace(x, "{NNNN}", "BBBB", 1)
        x = Replace(x, "{NNN}", "BBB", 1)
        x = Replace(x, "{NN}", "BB", 1)
        x = Replace(x, "{N}", "B", 1)
        x = Replace(x, "N", "X", 1)
        x = Replace(x, "B", "N", 1)
        For i As Integer = 1 To Len(x) - 1
            If Mid$(x, i, 1) = "{" Then
                d_brace.Add(Mid$(x, i + 1, 1), Mid$(x, i + 1, 1))
            End If
        Next
        x = Replace(x, "{", "")
        x = Replace(x, "}", "")
        Return x
    End Function
    Public Sub ConvertRule()
        For Each t As String In d_brace.Keys
            Dim i As Integer = IndexFor(t)
            If i > 0 AndAlso (Delim(i) <> "" Or i = n_fields) Then
                a_len(i) = 0
            End If
        Next
        For n As Integer = 1 To n_fields
            Dim c As String = UCase(a_token(n))
            If Not (c = "A" Or c = "P" Or c = "R" Or c = "S" Or c = "T" Or c = "N") Then
                If c = "X" Then
                    c = "N"
                End If
                If c = "I" Then
                    Prompt(a_token(n)) = "Node ID"
                Else
                    Dim v As String = ""
                    For j As Integer = 1 To a_len(n)
                        v = v & c
                    Next
                    Valid(a_token(n)) = v
                End If
            End If
        Next
    End Sub
End Class
